<?php

/**
 * This file is part of Switcher.
 * 
 * Switcher is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Switcher is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Switcher. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 */

namespace mustache;

/**
 * This class navigates through and extracts tags from the input. The <i>Stream</i> class
 * also acts as a bridge between the parser and the <i>ParseException</i>.
 * 
 * @author Dri <switcher.framework@gmail.com>
 * @license http://www.opensource.org/licenses/gpl-3.0.html GPL-3.0
 * @since 1.0.0
 * @see Parser
 * @see ParseException
 */
final class Stream
{
	/**
	 * The mustache template.
	 * @var string
	 */
	private $input;
	
	/**
	 * The cached length of the template.
	 * @var int
	 */
	private $inputLength;
	
	/**
	 * The position from which to read the template.
	 * @var int
	 */
	private $position = 0;
	
	/**
	 * The current delimiter for the tags.
	 * @var Delimiter
	 */
	private $delimiter;
	
	/**
	 * Constructor.
	 * 
	 * @param string $input The template to render
	 * @throws \InvalidArgumentException If $input is not a string
	 */
	public function __construct($input)
	{
		if ( !is_string($input) )
		{
			throw new \InvalidArgumentException('Not A String : ' . $input);
		}
		
		$this->input = $input;
		$this->inputLength = strlen($input);
		$this->delimiter = new Delimiter();
	}
	
	/**
	 * Creates a <i>ParseException</i>.
	 * 
	 * @param string $message The exception message
	 * @param int $position The position in the template
	 * @return ParseException A parse exception
	 */
	public function newParseException($message, $position = null)
	{
		if ($position === null)
		{
			$position = $this->position;
		}
		
		return new ParseException($message, $this->getPosition($position));
	}
	
	/**
	 * Creates a <i>ParseException</i> from a tag.
	 * 
	 * @param string $message The exception message
	 * @param object $tag The invalid tag
	 * @param \Exception $cause The optional cause of this exception
	 * @return ParseException A parse exception for the tag
	 */
	public function newTagParseException($message, $tag, $cause = null)
	{
		$position = $this->getPosition($tag->position);
		return new ParseException($message, $position, $cause);
	}
	
	private function getPosition($position)
	{
		$input = substr($this->input, 0, $position);
		
		preg_match('~^((.*)\n)?(.*)$~s', $input, $matches);
			
		return (object) array(
			'line' => substr_count($matches[1], "\n") + 1,
			'column' => strlen($matches[3]) + 1
		);
	}
	
	/**
	 * Set new delimiters from a <i>delimiter tag</i>.
	 * 
	 * @param object $tag The delimiter tag
	 * @throws ParseException If the tag is not a valid delimiter tag
	 */
	public function setDelimiter($tag)
	{
		if (preg_match('~^\s*([^= ]+)\s*([^= ]+)\s*\=$~', $tag->content, $matches) == 0)
		{
			throw $this->newTagParseException('Illegal delimiter : ' . $tag->content, $tag);
		}
		
		try
		{
			$this->delimiter->setTags($matches[1], $matches[2]);
		}
		catch(\InvalidArgumentException $e)
		{
			throw $this->newTagParseException($e->getMessage(), $tag, $e);
		}
	}
	
	/**
	 * Indicates if there is input to read.
	 * 
	 * @return bool True if there is more to read
	 * @throws \OverflowException If the current position exceeds the template size
	 */
	public function hasNext()
	{
		if ($this->position > $this->inputLength)
		{
			throw new \OverflowException('Position exceeds the size of the input');
		}
		
		return $this->position < $this->inputLength;
	}
	
	/**
	 * Finds the next tag if any, and returns it as an object. The tag object contains
	 * the following properties :
	 * <ul>
	 *  <li>tag : the whole tag, including the delimiters</li>
	 *  <li>type : the character defining the type of tag</li>
	 *  <li>content : the trimmed tag without the delimiters</li>
	 *  <li>position : the position in the template</li>
	 *  <li>length : the length of the whole tag</li>
	 * </ul>
	 * 
	 * @return mixed An object representing the tag or false
	 */
	public function getNextTag()
	{
		$start = $this->findStartDelimiter();
		
		if ($start === false)
		{
			return false;
		}
		
		$tag = $this->{$start->parser}($start->position);
		
		$this->makeStandalone($tag);
		
		return $tag;
	}
	
	/**
	 * Reads the input from the current position to the next tag, or to the end of the template.
	 * 
	 * @param mixed $tag The next tag or false
	 * @return string The input read
	 */
	public function readToTag($tag)
	{
		if ($tag === false)
		{
			$input = substr($this->input, $this->position);
			$this->position = $this->inputLength;
		}
		else
		{
			$input = substr($this->input, $this->position, $tag->position - $this->position);
			$this->position = $tag->position + $tag->length;
		}
		
		return $input;
	}
	
	public function rewind($position)
	{
		if ($position !== false)
		{
			$this->position = $position;
		}
	}
	
	/**
	 * Closes the section and goes accordingly to the right position.
	 * 
	 * @param SectionStack $sectionStack The section stack
	public function closeSection(SectionStack $sectionStack)
	{
		$rewind = $sectionStack->pop();
		
		if ($rewind !== false)
		{
			$this->position = $rewind;
		}
	}
	 */
	
	private function findStartDelimiter()
	{
		$normal = (object) array(
			'parser' => 'parseNormalTag',
			'position' => strpos($this->input, $this->delimiter->unquotedStart, $this->position)
		);
		
		$escape = (object) array(
			'parser' => 'parseEscapeTag',
			'position' => strpos($this->input, '{{{', $this->position)
		);
		
		if ($normal->position === $escape->position)
		{
			return $this->getPriorityTag($normal, $escape);
		}
		
		return $this->getFirstTag($normal, $escape);
	}
	
	private function getPriorityTag($normal, $escape)
	{
		if ($normal->position === false)
		{
			return false;
		}
		
		return $this->delimiter->precede ? $normal : $escape;
	}
	
	private function getFirstTag($normal, $escape)
	{
		if ($normal->position === false)
		{
			return $escape;
		}
		elseif ($escape->position === false)
		{
			return $normal;
		}
		
		return $normal->position <= $escape->position ? $normal : $escape;
	}
	
	private function parseNormalTag($position)
	{
		$start = $this->delimiter->start;
		$stop  = $this->delimiter->stop;
		$pattern = "~$start\s*(&|#|\^|/|\>|\=|\!)?\s*(.*?)$stop~s";
		$matches = $this->parseTag($pattern, $position);
		
		return (object) array(
			'tag' => $matches[0][0],
			'type' => $matches[1][0],
			'content' => $matches[2][0],
			'position' => $position,
			'length' => strlen($matches[0][0])
		);
	}
	
	private function parseEscapeTag($position)
	{
		$pattern = '~\{\{\{\s*(\.|([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*))\s*\}\}\}~';
		$matches = $this->parseTag($pattern, $position);
		
		return (object) array(
			'tag' => $matches[0][0],
			'type' => '&',
			'content' => $matches[1][0],
			'position' => $position,
			'length' => strlen($matches[0][0])
		);
	}
	
	private function parseTag($pattern, $position)
	{
		preg_match($pattern, $this->input, $matches, PREG_OFFSET_CAPTURE, $position);
		
		if (empty($matches) || $matches[0][1] != $position)
		{
			throw $this->newParseException('Invalid tag : ' . substr($this->input, $position, 20), $position);
		}
		
		return $matches;
	}
	
	private function makeStandalone($tag)
	{
		if (empty($tag->type) || strpos('#^/=!>', $tag->type) === false)
		{
			return;
		}
		
		$previousPart = $this->getTagPreviousPart($tag);
		$nextPart = $this->getTagNextPart($tag);
		
		if (trim($previousPart . $nextPart) == '')
		{
			$tag->tag = $previousPart . $tag->tag . $nextPart;
			$tag->position -= strlen($previousPart);
			$tag->length = strlen($tag->tag);
		}
	}
	
	private function getTagPreviousPart($tag)
	{
		$position = strrpos($this->input, "\n", $tag->position - $this->inputLength);
		$position = $position === false ? 0 : $position + 1;
		
		return substr($this->input, $position, $tag->position - $position);
	}
	
	private function getTagNextPart($tag)
	{
		$tagEndPosition = $tag->position + $tag->length;
		
		$position = strpos($this->input, "\n", $tagEndPosition);
		$position = $position === false ? $this->inputLength : $position + 1;
		
		return substr($this->input, $tagEndPosition, $position - $tagEndPosition);
	}
}

?>